# coding=utf-8
import serial  # package name: pyserial
from serial.tools.list_ports import comports
import os
import time
import keyboard
import tkinter.messagebox
import tkinter
import matplotlib.pyplot as plt
import re
import csv
import datetime
import schedule

# create .exe: pyinstaller -F main.py

class CMDUI(object):
    def __init__(self):
        self.runflag = True
        self.comname = None  # 当前串口号
        self.baundrate = None  # 当前波特率
        self.com = None  # com类
        self.ADC1 = None  # ADC1解码后
        self.ADC2 = None  # ADC2解码后
        self.ADC3 = None  # ADC3解码后
        self.ADC4 = None  # ADC4解码后
        self.rxlength = 72  # 一次接收72字节，可根据需要调节
        self.savedir = './Receive'  # 接收数据保存路径
        self.csvfile = None  # csv文件
        self.csvfilename = None  # csv文件名
        self.csvfiletime = None  # csv创建时间
        self.csvwriter = None  # csv writer
        self.plottimer = [0]  # 绘图的横坐标(时间t, 单位min)
        self.period_save = 30  # 自动保存周期, 单位sec
        self.period_plot = 1  # 绘图更新周期, 单位sec
        self.flag_plot = False
        self.period_display = 300  # 绘图显示长度，单位sec
        self.plotdata = [[0], [0], [0], [0], [0], [0], [0], [0],
                         [0], [0], [0], [0], [0], [0], [0], [0],
                         [0], [0], [0], [0], [0], [0], [0], [0],
                         [0], [0], [0], [0], [0], [0], [0], [0]]  # 绘图数据

    def run(self):
        print("Welcome!")
        '''连接串口'''
        print("===== System Start =====")
        while not self.com:
            self._connect()
        '''接收前初始化'''
        keyboard.add_hotkey('esc', self.__hotkey_esc)  # 设置热键
        schedule.every(self.period_save).seconds.do(self.__auto_saving)  # 设置定时执行: 自动保存csv
        schedule.every(self.period_plot).seconds.do(self.__flag_plot_True)  # 设置定时执行: 更新图像
        self.savedir_init()  # 初始化保存路径
        plt.ion()  # interactive mode on (不同于MATLAB的hold on)
        '''开始接收'''
        print("===== Start Receiving =====")
        while self.runflag:
            self._receive()
            schedule.run_pending()

    def stop_receive(self):
        self.runflag = False

    def savedir_init(self):
        if not os.path.exists(self.savedir):
            os.mkdir(self.savedir)
        self.csvfiletime = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d_%H-%M-%S')
        self.csvfilename = 'comdata_{}.csv'.format(self.csvfiletime)
        self.csvfile = open('{}/{}'.format(self.savedir, self.csvfilename), 'a', newline='')
        self.csvwriter = csv.writer(self.csvfile)
        print("[INFO] Receive start from {}, save as {}/{}".format(self.csvfiletime, self.savedir, self.csvfilename))

    def _receive(self):
        try:  # 尝试接收
            counter = time.time()
            rxdata = self.com.read(self.rxlength).hex()  # 转换为十六进制
            print("\rReceive: {}... ({}bytes/{:.3f}s)".format(rxdata[0:50], self.rxlength,
                                                              round(time.time() - counter, 3)), end='')
        except serial.serialutil.SerialException:  # 若连接中断
            self.com = None
            print("\r[ERROR] Connection lost! Please check your cable and connection! Trying to reconnect...")
            while not self.com:  # 尝试重连
                try:
                    self.com = serial.Serial(self.comname, self.baundrate, timeout=1)
                except serial.serialutil.SerialException:
                    time.sleep(0.1)
                else:
                    print("[INFO] Successfully reconnect to {} at {} baundrate!".format(self.comname, self.baundrate))
        else:  # 若接收成功
            if rxdata:  # 若rxdata不为空
                self._decode_display(rxdata)

    def _decode_display(self, rxdata):
        """
        解码数据，保存csv
        :return:
        """
        locater1 = str(rxdata).find('fff1') + 4
        locater2 = str(rxdata).find('fff2') + 4
        locater3 = str(rxdata).find('fff3') + 4
        locater4 = str(rxdata).find('fff4') + 4
        rx1 = str(rxdata[locater1:locater2 - 4])
        rx2 = str(rxdata[locater2:locater3 - 4])
        rx3 = str(rxdata[locater3:locater4 - 4])
        rx4 = str(rxdata[locater4:])
        # x[2:]+x[:2]: abcd->cdab, /6553.5: transform to floatpoint number, findall(r'.{4}', rx1): split str every 4 char
        rx1 = list(map(lambda x: round(int(x[2:] + x[:2], 16) / 6553.5, 6), re.findall(r'.{4}', rx1)))
        rx2 = list(map(lambda x: round(int(x[2:] + x[:2], 16) / 6553.5, 6), re.findall(r'.{4}', rx2)))
        rx3 = list(map(lambda x: round(int(x[2:] + x[:2], 16) / 6553.5, 6), re.findall(r'.{4}', rx3)))
        rx4 = list(map(lambda x: round(int(x[2:] + x[:2], 16) / 6553.5, 6), re.findall(r'.{4}', rx4)))
        if len(rx1) == 8 and len(rx2) == 8 and len(rx3) == 8 and len(rx4) == 8:
            self.ADC1 = rx1
            self.ADC2 = rx2
            self.ADC3 = rx3
            self.ADC4 = rx4
        try:
            self._save_tocsv(self.ADC1 + self.ADC2 + self.ADC3 + self.ADC4)
            if self.flag_plot is True:
                self._update_plot()
                self.flag_plot = False
        except TypeError:
            # TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
            # 开启后第一次存储可能会接收失败
            pass

    def _save_tocsv(self, data):
        try:
            self.csvwriter.writerow(data)  # 写入一行
        except ValueError:
            if self.runflag:
                print("\n[ERROR] CSV file has been closed.\n")

    def _write(self):
        # result = self.com.write(" ".encode("gbk"))
        # print("写总字节数:", result)
        pass  # TODO 通过串口写数据

    def _connect(self):
        comlist = list(comports())
        if len(comlist):  # 若存在可用串口
            '''选择串口'''
            comlist = list(map(lambda x: list(x), comlist))
            print("[INFO] Available COMs are listed below:")
            if len(comlist) == 1 & str(comlist[0][1]).find('CH340') != -1:  # 若只存在一个串口且是CH340
                print(comlist)  # 打印出所有可用的串口，选择描述为“USB-SERIAL CH340”的设备
                self.comname = comlist[0][0]
                print("[INFO] Use {}.".format(self.comname))
            elif len(comlist) == 1 & str(comlist[0][1]).find('CH340') == -1:  # 若只存在一个串口且但不是CH340
                print(comlist)  # 打印出所有可用的串口
                print('[ERROR] COM of CH340 is not found! Please check your connection or driver. Press Enter to retry.')
                os.system('pause')
            else:  # 若存在多个串口
                self.comname = comcandidate = ''
                for item in comlist:
                    print(item[0], item)  # 打印出所有可用的串口，引导用户做出选择
                    comcandidate = comcandidate + str(item[0])
                while self.comname == '':
                    self.comname = input("[INPUT] Which one to choose? Please type in a COM (for example: COM1):")
                    if comcandidate.find(self.comname) == -1:
                        print("[ERROR] {} is not exist! Please check your input and retry.".format(self.comname))
                        self.comname = ''
                    else:
                        print("[INFO] Use {}.".format(self.comname))
            '''选择波特率'''
            # self.baundrate = ''
            # while self.baundrate == '':
            #     self.baundrate = input("[INPUT] Please type in a baundrate (press Enter to use default value 256000): ")
            #     if self.baundrate == '':  # 波特率默认值为256000
            #         self.baundrate = 256000
            #     else:
            #         try:
            #             self.baundrate = int(self.baundrate)
            #         except ValueError:
            #             print("[ERROR] Please input an legal number!")
            #             self.baundrate = ''
            self.baundrate = 256000
            '''尝试连接'''
            try:
                print("[INFO] Try to connect to {} at {} baundrate...".format(self.comname, self.baundrate))
                self.com = serial.Serial(self.comname, self.baundrate, timeout=1)
            except Exception as e:
                print("[ERROR]", e)
                print("Press Enter to retry.")
                os.system('pause')
            else:
                print("[INFO] Successfully connect to {} at {} baundrate!".format(self.comname, self.baundrate))
                '''出口'''
        else:  # 若不存在串口
            print("[ERROR] COM does not exist! Press Enter to retry.")
            os.system('pause')

    def _update_plot(self):
        """
        更新绘图
        """
        cnames = ('aliceblue','antiquewhite','aqua','aquamarine','azure','beige','bisque','black','blanchedalmond',
                  'blue','blueviolet','brown','burlywood','cadetblue','chartreuse','chocolate','coral','cornflowerblue',
                  'cornsilk','crimson','cyan','darkblue','darkcyan','darkgoldenrod','darkgray','darkgreen','darkkhaki',
                  'darkmagenta','darkolivegreen','darkorange','darkorchid','darkred')
        plt.cla()
        self.plottimer.append(self.plottimer[-1] + self.period_plot)
        try:
            '''散点图'''
            # for i, x in enumerate(self.ADC1 + self.ADC2 + self.ADC3 + self.ADC4):
            #     plt.scatter(self.plottimer[-1], x, color=cnames[i])
            # plt.pause(0.001)
            '''折线图'''
            for ADCi, ADC in enumerate([self.ADC1, self.ADC2, self.ADC3, self.ADC4]):
                try:
                    for datai, data in enumerate(ADC):
                        self.plotdata[ADCi * 8 + datai].append(data)
                        if len(self.plottimer) > self.period_display:
                            plt.plot(self.plottimer[:self.period_display],
                                     self.plotdata[ADCi * 8 + datai][len(self.plotdata[ADCi * 8 + datai])-self.period_display-1:-1],
                                     color=cnames[ADCi * 8 + datai])
                        else:
                            plt.plot(self.plottimer, self.plotdata[ADCi * 8 + datai], color=cnames[ADCi * 8 + datai])
                except TypeError:
                    print('[ERROR] Plot error!')
            plt.pause(0.001)
        except IndexError:
            print("\n[ERROR] Com error.\n")

    def __flag_plot_True(self):
        self.flag_plot = True

    def __auto_saving(self):
        """
        close and reopen, to save csv file regularly
        """
        self.csvfile.close()
        self.csvfile = open('{}/{}'.format(self.savedir, self.csvfilename), 'a', newline='')
        self.csvwriter = csv.writer(self.csvfile)
        # Thread(target=shutil.copy, args=[self.savedir+'/'+self.csvfilename, self.savedir+'/'+self.csvfilename+'.{}.bkp'.
        #        format(datetime.datetime.strftime(datetime.datetime.now(),'%Y-%m-%d_%H-%M-%S'))]).start()

    def __hotkey_esc(self):
        """
        热键函数，按键弹窗，不会阻塞主进程
        """
        top = tkinter.Tk()
        top.geometry('0x0+999999+0')
        res = tkinter.messagebox.askyesno("提示", "要执行此操作？")
        top.destroy()
        if res is True:
            self.csvfile.close()
            self.stop_receive()
            # sys.exit()  # 只能退出tk进程，无法退出主进程


if __name__ == '__main__':
    print("\n====================================================================")
    print("\n注意！程序目录不能有中文！")
    print("\nNotice! No Chinese in the directory! ")
    print("\n====================================================================\n\n")
    ui = CMDUI()
    ui.run()
